Skip to content
This repository was archived by the owner on Jan 3, 2023. It is now read-only.

Change config file name - #160

Open
sandmanstorm wants to merge 258 commits into
Chocobozzz:masterfrom
arvage:master
Open

Change config file name#160
sandmanstorm wants to merge 258 commits into
Chocobozzz:masterfrom
arvage:master

Conversation

@sandmanstorm

Copy link
Copy Markdown

It would be nice to change the client.ovpn file name to "username".ovpn

arvage and others added 30 commits June 4, 2026 16:48
README:
- Rewrite title and summary to reflect current feature set
- Add feature table: dashboard, cert management, SMTP, roles, notifications
- Update supported platforms table (Ubuntu 22/24, Debian 11/12, RPi OS 32/64-bit)
- Add Admin Panel section documenting every page and its functionality
- Document certificate management workflow and sudoers requirement
- Document role-based access (super-admin vs read-only)
- Document email notification setup and requirements
- Update update.sh section to describe backup, migration, and auto-patching
- Update uninstall.sh section to describe confirmation prompt and scope
- Add How It Works architecture diagram
- Update libraries table: Bootstrap 5, Bootstrap Icons, bootstrap-table 1.22,
  jQuery 3.7; remove Bootstrap Datepicker and X-editable (no longer used)
- Remove Bower/npm references throughout
- Add Reporting Issues section with required info checklist

CHANGELOG:
- Add v1.0.0 entry covering all changes made in this release

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ctions

functions.php line 88: str_contains($line, "\t") requires PHP 8.0+;
  replaced with strpos($line, "\t") !== false (PHP 5+)

functions.php line 130: match($status){...} requires PHP 8.0+;
  replaced with array lookup using isset() ternary (PHP 7.0+)

mailer.php lines 23-24: fn() => arrow function syntax requires PHP 7.4+;
  replaced with regular closures using use(&$conn) (PHP 5.3+)

All PHP files now require PHP 7.2+ at minimum, covering:
  Debian 10 (PHP 7.3), Debian 11 (PHP 7.4), Debian 12 (PHP 8.2),
  Ubuntu 20.04 (PHP 7.4), Ubuntu 22.04 (PHP 8.1), Ubuntu 24.04 (PHP 8.3),
  Raspberry Pi OS Bullseye (PHP 7.4), Bookworm (PHP 8.2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The sed -i on /etc/needrestart/needrestart.conf was running without sudo.
online-install.sh runs as a regular user so it cannot write to /etc/,
causing: "sed: couldn't open temporary file /etc/needrestart/sedcXXXXX:
Permission denied"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous approach (sed -i on /etc/needrestart/needrestart.conf) failed
with "Permission denied" even with sudo because sed -i creates a temp file
in the target directory, which can fail in certain pipe/sudo contexts.

New approach: export NEEDRESTART_MODE=a before any apt-get calls.
needrestart respects this environment variable and runs in automatic mode
(no interactive prompts) without requiring any file to be written.

- online-install.sh: remove needrestart config file edit; export
  NEEDRESTART_MODE=a and DEBIAN_FRONTEND=noninteractive at the top;
  use sudo -E for all apt-get and install.sh calls so the environment
  is preserved through sudo
- install.sh: add export NEEDRESTART_MODE=a alongside DEBIAN_FRONTEND
  so it is suppressed when install.sh is run directly as well

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a machine has multiple PHP versions installed (e.g. php7.4 from a
prior attempt and php8.4 from the current install), Apache may load an
old PHP module that doesn't match the CLI PHP version, causing parse
errors for version-specific syntax.

install.sh: before enabling the target PHP module, iterate over all
currently loaded PHP modules (via apache2ctl -M) and disable any that
don't match the CLI PHP version. This prevents conflicts when upgrading
from a previous installation.

update.sh: after updating files, detect the Apache-loaded PHP module
version and compare it to the CLI PHP version. If they differ, disable
the old module and enable the correct one before restarting Apache.
This automatically self-heals the mismatch on every update run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sidebar: add 'by Armin' GitHub link below the signed-in user block,
styled as a subtle credit with hover highlight.

Login page: add small 'by Armin - github.com/arvage/OpenVPN-Admin'
link below the login card.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… runner

ADD COLUMN IF NOT EXISTS is MariaDB-only syntax. MySQL raises error 1064
(syntax error) when it encounters it, breaking the migration on any
MySQL-backed installation.

sql/schema-10.sql: remove IF NOT EXISTS from all four ALTER TABLE ADD
COLUMN statements. CREATE TABLE IF NOT EXISTS is kept (valid MySQL syntax).

include/functions.php: add execMigrationSql() helper that splits a SQL
file on semicolons and executes each statement individually, silently
skipping MySQL error 1060 (Duplicate column name) and 1050 (Table already
exists). This makes migrations idempotent and safe to re-run on both
MySQL and MariaDB without IF NOT EXISTS support.

migration.php: switch from bdd->exec($sql) to execMigrationSql() so the
per-statement error handling applies to all future migrations.

index.php: same change in the first-time web installer flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous check (SHOW TABLES LIKE 'admin') returned true as soon as
the admin table was created by the SQL migration, even when no admin
users existed yet. This caused the installation page to redirect to
login immediately after a fresh migration run, with no way to create
the first admin account.

Now queries COUNT(*) FROM admin instead — returns false if the table
is missing (exception caught) or empty, true only when at least one
admin user exists. This correctly represents the real "installed" state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove redundant redirect-gateway directive from all client templates;
the server already pushes redirect-gateway def1 via server.conf.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace deprecated comp-lzo with compress lzo on server and all client
templates. The old directive uses a different framing format in newer
OpenVPN builds, causing "Bad LZO decompression header byte" mismatches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The server was being migrated to compress lz4-v2 by install.sh/update.sh
but client configs kept comp-lzo, causing "Bad LZO decompression header
byte" errors. Change all templates to compress lz4-v2 and add client
config patching to both install.sh and update.sh so server and clients
always stay in sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e email editing

- Dashboard already reads OpenVPN status file and auto-refreshes every 10s; user
  table now also refreshes every 10s on the Users page
- Notification bell in topbar (super-admin only): shows unread badge and dropdown
  with last 50 events; notifications fire on user add, edit, and delete; clicking
  bell or "Mark all read" clears the unread state per-admin
- Profile button in topbar opens a modal for any admin to update their own email
  (stored in admin_mail); email pre-fills from the database on modal open
- Schema-11 migration adds the notification table; new installations pick it up
  automatically; existing installs run: php migration.php /path/to/www

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Path traversal (RCE):
- update_config now validates config_file against an explicit allowlist of the 4
  permitted paths; arbitrary file writes are no longer possible

Password hash exposure:
- user_pass and admin_pass are no longer included in ?select=user / ?select=admin
  API responses; hashes never leave the server

Authorization bypass (cert download):
- cert_download handler now calls requireSuperAdmin(); read-only admins can no
  longer download private key material

CSRF:
- generateCsrfToken() / verifyCsrfToken() added to functions.php
- CSRF token generated per-session and exposed as window.CSRF_TOKEN in index.php
- jQuery $.ajaxPrefilter automatically attaches the token to every POST request
- grids.php validates the token at the top of all POST handling

Stored XSS (certificate CN):
- listCertificates() now htmlspecialchars() the CN before returning JSON
- loadCertificates() JS rebuilt with safe DOM APIs ($().text(), $().attr()) instead
  of string concatenation

Stored XSS (user email / phone / admin fields):
- userActionsFormatter, adminActionsFormatter, passFormatter, adminPassFormatter
  all rebuilt with jQuery DOM construction and .attr() to prevent attribute
  injection; no user data is concatenated into raw HTML strings

Privilege escalation (fail-open role):
- getCurrentAdminRole() now returns 'read-only' (not 'super-admin') when the role
  field is empty or when a DB exception occurs; system fails closed

Session cookies:
- session_set_cookie_params() with httponly=true, samesite=Strict, secure=auto
  added before session_start() in both index.php and grids.php

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New sidebar page (Management > Fail2Ban) for super-admins:
- Reads all configured jails and their banned IP lists via fail2ban-client
- Summary stats: currently banned count, active jail count, currently failing
- Per-jail cards showing banned IPs with red header when bans are active
- Unban button per IP (confirm dialog, instant reload)
- Manual Ban IP modal with IP input + jail selector
- Auto-refreshes every 10 seconds when the page is active
- Manual refresh button

Backend (include/grids.php):
- GET ?select=fail2ban  → getFail2BanStatus() (super-admin only)
- POST ban_ip           → validates IP (FILTER_VALIDATE_IP) and jail name
                          (/^[a-zA-Z0-9_-]+$/) then calls fail2ban-client
- POST unban_ip         → same validation, calls fail2ban-client unbanip

Security:
- IP addresses validated with filter_var(FILTER_VALIDATE_IP)
- Jail names validated with strict regex before shell use
- escapeshellarg() wraps all shell parameters
- Both ban/unban require super-admin role

Setup: web server needs passwordless sudo for fail2ban-client:
  echo "www-data ALL=(ALL) NOPASSWD: /usr/bin/fail2ban-client" \
    | sudo tee /etc/sudoers.d/openvpn-admin-fail2ban
  sudo chmod 440 /etc/sudoers.d/openvpn-admin-fail2ban

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fail2Ban:
- Added fail2ban package to apt-get install for Ubuntu, Debian, and Raspbian
- Extended /etc/sudoers.d/openvpn-admin to grant the web server user
  passwordless sudo access to /usr/bin/fail2ban-client (required by the
  Fail2Ban admin page)
- Writes /etc/fail2ban/filter.d/openvpn.conf: matches TLS auth errors,
  AUTH_FAILED, and TLS key negotiation failures in the OpenVPN server log
- Writes /etc/fail2ban/jail.d/openvpn-admin.conf: enables sshd and openvpn
  jails with 5-attempt / 1-hour-ban defaults; port and protocol are set from
  the installer variables so they match the configured VPN setup
- Enables and restarts fail2ban service
- Summary output now mentions fail2ban is active and points to the admin panel

Interactive prompts:
- Removed `read -t 120` timeout from the public-IP and VPN-name prompts; the
  120-second wait persisted even after the user pressed Enter because `read -t`
  continues running until its timer expires regardless of input. Both prompts
  now use plain `read -p` which returns immediately when Enter is pressed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- sudoers block always writes both entries (EasyRSA + fail2ban-client)
  unconditionally, replacing the old "only if missing" guard — ensures
  existing installations pick up the fail2ban-client permission on update
- Installs fail2ban via apt-get if fail2ban-client is not already in PATH
- Reads port and protocol from the existing /etc/openvpn/server.conf so
  the OpenVPN jail matches whatever the server is actually running on
- Writes /etc/fail2ban/filter.d/openvpn.conf and
  /etc/fail2ban/jail.d/openvpn-admin.conf (idempotent: safe to overwrite
  on every update run)
- Enables and restarts fail2ban; skips gracefully if not available

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
README:
- Add Fail2Ban, In-app Notifications, and Admin Profile to features table
- New Fail2Ban section under Admin Panel (live status, unban, manual ban,
  sudo setup note)
- New Profile section under Admin Panel
- Updated Users section to mention in-app notifications
- Updated installer section to list what is set up automatically (fail2ban,
  sudoers for fail2ban-client)
- Updated update.sh description to include fail2ban and sudoers changes
- New Security section with a hardening table covering all 7 fixes
- Updated How It Works diagram to include fail2ban and notification table
- Added fail2ban version to Reporting Issues checklist

CHANGELOG:
- New 1.1.0 entry covering: fail2ban integration, in-app notifications,
  admin profile editing, dashboard auto-refresh, 7 security fixes,
  installer prompt bug fix, and update.sh sudoers idempotency fix

ISSUE_TEMPLATE:
- Added fail2ban version field
- Added OpenVPN version field
- Added admin panel version field
- Added structured steps-to-reproduce section
- Added fail2ban log snippet field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…date

Adds a git pull step at the start of update.sh so that running
  sudo ./update.sh /var/www
is now the single command needed to go from any state to fully up-to-date
(previously required a manual git pull first, which led to the notification
table migration being skipped).

The pull runs as the repo directory owner rather than root to avoid
changing file ownership of the repo. Falls back gracefully with a warning
if git is unavailable or the pull fails, so the rest of the update
(file copy, migrations, fail2ban config, Apache reload) still runs
against whatever local files are present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JS bugs fixed:
- refreshNotifications() was never called after add/edit/delete user — the
  badge would only update on the 30-second poll, making it appear as if
  notifications weren't working. Now called immediately in all three
  success callbacks (add, edit via $.when.done, delete in confirm modal).
- Bell click now calls refreshNotifications() unconditionally before
  mark-read, so the dropdown is always fresh when opened regardless of
  whether the 30-second interval has fired.

PHP bug fixed:
- ?select=notifications catch block was returning is_super:false when the
  notification table didn't exist, which caused JS to bail silently and
  leave "Loading…" stuck in the dropdown forever. Now returns is_super:true
  with setup_needed:true so JS shows an actionable message instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ifications

Settings → Notifications now has a second section (Admin Notifications) with
three toggle switches below the existing email notification toggles:
  - User Added    (default on)
  - User Edited   (default off — too frequent/noisy)
  - User Deleted  (default on)

These control whether the in-app bell notification is created when a super-admin
performs the corresponding user action.

Implementation:
- schema-12.sql: adds notify_admin_user_add, notify_admin_user_edit,
  notify_admin_user_delete columns (TINYINT DEFAULT 1/0/1) to smtp_settings
- createNotification() now queries smtp_settings before inserting; if the
  toggle for that event type is off, the notification is silently skipped
- save_smtp handler persists the three new fields alongside existing ones
- loadSmtpSettings() in JS reads and applies the new values on page load,
  defaulting to add=on / edit=off / delete=on when the column is not yet
  present (pre-migration)
- notif-form submit in JS sends the three new fields to save_smtp

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each row in the Web Admins table now has a pencil (edit) button that
opens a small modal pre-populated with the admin's current email address.
Saving POSTs set_admin with name=admin_mail, which the existing backend
already accepts — no PHP changes needed.

The role-toggle and delete buttons are unchanged and still hidden for the
current admin's own row. The edit email button is shown for all rows
(including self) so a super-admin can also correct their own email from
the grid in addition to the Profile button in the topbar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
README:
- Admins section now describes the three per-row action buttons
  (edit email, toggle role, delete) and notes that email can be
  edited for any admin from the grid
- Profile section updated to mention super-admins can also edit
  other admins' emails from the Admins grid
- Settings → Notifications split into two groups: email notifications
  and admin in-app notification toggles with defaults documented
- Update section simplified: update.sh now auto-pulls from git so
  the separate git pull step is no longer needed

CHANGELOG:
- New 1.1.1 entry covering: admin email edit in grid, per-type
  notification toggles, notification bell fixes (immediate refresh,
  setup_needed message, fresh on click), and update.sh auto-pull

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- IP Banned and IP Unbanned events now create in-app notifications for
  super-admins, consistent with user add/edit/delete notifications
- Two new toggles in Settings → Notifications (Admin Notifications section):
    IP Banned  — default on
    IP Unbanned — default on
  Backed by notify_admin_ban and notify_admin_unban columns (schema-13)
- Bell updates immediately after a ban or unban action (refreshNotifications()
  called in both success callbacks, same pattern as user CRUD)
- Notification dropdown renders shield-x (red) for bans and
  shield-check (green) for unbans

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Content-Security-Policy header (script-src 'self') blocked the
inline <script> block that set window.ADMIN_ROLE, window.CURRENT_PAGE,
and window.CSRF_TOKEN. This caused isSuperAdmin to always be false
(hiding all edit/delete/reset buttons) and currentPage to default to
'dashboard' (keeping the Fail2Ban page stuck on the loading spinner).

Moved the three window variables into js/config.php, a PHP-served
external script that satisfies the 'self' CSP directive. Also added
filemtime-based cache busting to grids.js and index.css.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ocked by apache2 sandboxing; simplify Client Config editor to a single tab

- install.sh matched the wrong placeholder (xxx.xxx.xxx.xxx) against the
  actual template placeholder (10.10.100.27), so the public IP/hostname was
  never substituted into shipped .ovpn files.
- Ubuntu 26.04+'s apache2.service hides /etc/sudoers(.d) from Apache and
  everything it spawns, breaking the web UI's sudo-based fail2ban-client and
  easyrsa calls no matter what's in sudoers.d. install.sh now installs a
  systemd override that restores visibility of sudoers for this unit only,
  when the packaged unit has the restriction.
- Client Configuration Editor: removed the GNU/Linux and macOS/Viscosity
  tabs, keeping only the shared .ovpn template under a single "Editor" tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… sudo fix, Config editor)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment out the lz4-v2 compression setting and enable default compression.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants